iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Modern Web

前端開發之那些我會的與我不會的技術系列 第 20

了解React中的useRef Hook:特性和應用

  • 分享至 

  • xImage
  •  

今天要來介紹的是useRef這個hook,在使用的時候可能常常會和useState搞混,但是事實上它們的使用情境是很大的不同的,接下來來一一介紹useRef的特性與使用方式。

使用

const ref = useRef(initialValue)

使用useRef會回傳一個物件,帶有唯一一個屬性current 並賦予一開始帶入的initialValue,接下來要變更值的時候可以直接進行修改,ref.current = 變更值,還有個很重要的地方就是,更新值後並不會像useState的setter function一樣觸發重新渲染,而且也不建議在元件渲染時讀寫current,這是因為變更current 不會觸發渲染,所以在渲染得到的值可能跟你想像的會不太一樣。與useStatey的差別比較表如下

useRef useState
回傳值 回傳一個帶有current屬性的物件,current的值為帶入的useRef的值 回傳一個帶有值和setter function的陣列
更新方式 直接變更current的值 使用setter function
渲染 變更值不會重新渲染 使用setter更新值後重新渲染
盡可能不要在渲染的時候變更值 可以在渲染的時候變更值

我們可以看以下這個簡單的範例,當current + 1的時候console出來的數字有持續+1,但是畫面上就是沒有改變的,就是因為這個值變更了不會讓元件重新渲染。

export default function MyApp() {
  const ref = useRef(0);
  return (<>
    <button onClick={() => {
      ref.current = ref.current + 1
      console.log(ref)
    }}>
    click to + 1
    </button>
    {ref.current}
  </>);
}

用在什麼地方

useRef回傳的物件就如同Js的物件一樣沒有像是setter function那樣重新渲染的功能,它就像是React提供一個跳脫React狀態更新並儲存值的一個方式,由於擁有不會重新渲染的特性,所以適用於不需要畫面更新的功能,像是

  • 儲存setTimeout和setInterval的回傳值
const ref = useRef();
ref.current = setTimeout(() => {}, 1000);
clearTimeout(ref.current); // 可以用來儲存timeout id,以便之後可以clear
  • 直接存取DOM node操作瀏覽器提供的API

在JSX的屬性ref帶入useRef的回傳值,就可以得到DOM node

const inputRef = useRef();
<input ref={inputRef} />
// inputRef.current可以取得input DOM

取得input並且focus

import {useRef} from 'react'

export default function MyApp() {
  const inputRef = useRef(null);
  return (<>
    <button onClick={() => {
      inputRef.current.focus()
    }}>focus input</button>
    <input type="text" ref={inputRef} />
    </>
  );
}

雖然建議不要在渲染的時候讀寫useRef值,但是在某些情形下使用是合理的,但要注意的是確保每次回傳的結果是相同的 ,如下範例playerRef.current都會回傳一個video。

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
//...
}

ref callback

如果要操作數個element,例如我們想要拿到所有ul底下li的DOM node,我們無法用for迴圈的方式來達到,因為hook只能在最上層使用不能用在條件或是迴圈內。

// x 錯誤範例
<ul>
  {items.map((item) => {
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

除了手動宣告一堆useRef並且一個一個加在ref屬性外,我們可以使用JSX的ref屬性的ref callback來達到拿到數個DOM node的效果。直接看官方的文件範例,它在ref帶入一個function,並且帶有一個該element的node參數,可以透過這個callback將個別li的node分別存到useRef賦予的map裡。

import { useRef } from 'react';

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}

什麼時候會呼叫這個callback function

會有兩個時機呼叫這個function,就如同在更新畫面的時候會經過兩個階段,一個是渲染的時候,另個是node實際畫到畫面上的時候(commit)都會呼叫callback。但只有commit那一次帶的參數是實際的node,它會在畫上畫面之後馬上呼叫,但渲染那次得到的是null,跟文件的理念一致不在渲染的時候存取useRef值,實際上在渲染階段也還沒有拿到實際的node。

使用forwardRef開放出自己的ref

在預設我們自己定義的元件是不允許ref這個屬性,會出現以下的錯誤。

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

// 錯誤範例
function MyComponent({ref}) {
	return <input type="text" ref={ref} /> // 其實只要prop不是ref這個名稱就可以用,但是這不是個好方法
}
function App() {
	const inputRef = useRef(null);
	return <MyComponent ref={inputRef}/>
}

會有這樣的錯誤是,React認為元件如果暴露它的的ref提供存取,會讓的code變得很脆弱容易有問題,於是如果需要這種功能的話就需要opt in的方式,將ref給開放出來供其他元件使用。

使用forwardRef帶入一個元件參數,元件帶有兩個參數,第一個是props,第二個參數是傳入的ref

const SomeComponent = forwardRef(render)

import { forwardRef } from 'react';

const MyInput = forwardRef(({ value, onChange }, ref) => {
  return (
    <input
      value={value}
      onChange={onChange}
      ref={ref}
    />
  );
});

export default MyInput;

使用useimperativehandle只開放指定的方法

如果forwardRef搭配useimperativehandle使用可以指定要暴露給其他元件存取的方法。如同以下範例,只有開方focus方法並且自己撰寫實際執行的程式。這時候在父層使用ref.current.focus就會執行在useImperativeHandle裡面定義好的focus function。

import { forwardRef, useRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus();
      },
    };
  }, []);

  return <input {...props} ref={inputRef} />;
});

參考

https://react.dev/reference/react/useRef
https://react.dev/learn/referencing-values-with-refs
https://react.dev/learn/manipulating-the-dom-with-refs


上一篇
React中的效能優化:了解 useCallback 和 useMemo Hooks
下一篇
React Hook: useLayoutEffect的使用與注意事項
系列文
前端開發之那些我會的與我不會的技術31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言